iOS中使用autolayout来进行UITableView的布局(1)

为了更好的使用Autolayout,在开发过程中发现了需要使用UITableViewCell,所以网上爬文。以下是翻译

原文链接:点我跳转

使用不同高度的TableViewCell

[我的这篇文章曾经介绍了iOS8实现这个方式的一个更简单的方法]

我曾经发过文章来介绍使用动态类型来增加因为字体大小增大而增大的单元格(cell)的高度。然而这能对那些有着一样高度的单元格(cell)的tableView有效。这篇文章中,我将一步步来使用autolayout来设置不同的cell高度。

在一切开始之前,我将感谢Amy Worrall的这篇文章,因为他启发了我,让我保存了一个Cell的属性,从而使用tableView:heightForRowAtIndexPath:方法来根据内容和获得正确的大小。

Huckleberry(不知道咋翻译ORZ,就当做内容来源来源)


作为数据源(Source data)我的内容,我选择了Project Gutenberg中的前十五章——“The Adventures of Huckleberry Finn”,并将它导入到plist文件中(每一句作为一项)。这个给予我了近2000行不同长度的内容。这是为了展示每个在对应的tableView中的每个句子都有对应的行号。

图片1

每一个单元格(cell)的高度被要求了不同的行数,这在使用更大的字体设置时更加明显:

图片2

开始


我们以Xcode中的单一应用(single View Application)作为基础,通过一定的改变,使他有一个tableViewController,并嵌入一个UINavigationController中,如下面所示的:

图片1

AutoLayout来设置保存的Cell原型

为句子的Label增加Autolayout约束,分别约束左(leading),右(trailing),下(bottom)的介于单元格(cell)和内容之间的空间。如果你对在inderface Builder中使用autolayout不太熟悉的话,还有一些方法来增加约束(在写这篇文章的时候我是用的是Xcode 5.0.2)。你可以在storyboard中点击control按钮同时拖动label到包含它的内容或者在左边的document outline中右键拖动并选择左(leading),右(trailing),下(bottom)

图片2

根据你的label的位置,你可能需要调整你的约束来确保他们有一个默认的空间。你能够在右侧的检测器中中检查约束同时设置相应的值。

图片3

添加进一步的约束在两个label之间来设置他们的垂直空间来设置标准距离,重复以上过程来设置行号和包含他的空间的左、右、上的空间约束。interface builder将报错,你这两个labels在垂直方向有歧义的layout,你能看到这么问题在storyboard左侧的document outline。如果你点击了那个红色的按钮:
图片4

autolayout将不知道我们将会调整内容的大小来保证每个label都合适,因此他希望知道一个暗示来明白哪个label首先应该收缩还是扩张来适应这个空间。为了移除warning,我们将设置行号(label)的较低的垂直压缩阻力同时增加垂直拥抱来是他保持在我们想要的大小
图片5

如果你设置好了以上约束,他看起来大概是这样:
图片6
图片7

创建类来自定义单元格(cell)

创建一个一个新的OC类文件到工程中来自定义tableViewCell,这个类只创建了IBOutlet原型来保存这两个Label

1
2
3
4
5
6
7
8
9
10
11
// UYLTextCell.h
#import <UIKit/UIKit.h>
@interface UYLTextCell : UITableViewCell
@property (nonatomic, weak) IBOutlet UILabel *numberLabel;
@property (nonatomic, weak) IBOutlet UILabel *lineLabel;
@end
// UYLTextCell.m
#import "UYLTextCell.h"
@implementation UYLTextCell
@end

在storyboard中设置这个类并将storyboard中的两个Label连接到两个IBOutlet中,同时设置它(uitableViewCell)的identifier:

图片8

设置TableView的DataSource

创建私有的priviate原型在VC中来保存我们的数据:

1
2
3
@interface UYLTableViewController ()
@property (nonatomic, strong) NSArray *sourceData;
@end

这些内容保存在名叫:SourceData.plist的plist文件中,当调用sourceData的getter方法的时候,这将被导入到内存中。

1
2
3
4
5
6
7
8
9
- (NSArray *)sourceData
{
if (!_sourceData)
{
NSString *filePath = [[NSBundle mainBundle] pathForResource:@"SourceData" ofType:@"plist"];
_sourceData = [NSArray arrayWithContentsOfFile:filePath];
}
return _sourceData;
}

这个tableViewController必须强制性实现2个UITableViewDataSource的方法,第一个返回section中的行数,这里我们返回sourceData这个数组的大小

1
2
3
4
- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
return [self.sourceData count];
}

第二个方法是返回TableView的Cell从一个特殊的地方(译者注:如果没有生成则从内存池中,否则创建)

1
2
3
4
5
6
- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
UITableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:UYLCellIdentifier forIndexPath:indexPath];
[self configureCell:cell forRowAtIndexPath:indexPath];
return cell;
}

里面的UYLCellIdentifier变量是一个静态NSString类,这是为了匹配storyboard中我们设置的值:

1
static NSString *UYLCellIdentifier = @"UYLTextCell";

configureCell方法将会设置cell的内容,你将会明白为什么这是一个好的方法来将代码放在单独的较短的方法中:

1
2
3
4
5
6
7
8
9
10
11
- (void)configureCell:(UITableViewCell *)cell forRowAtIndexPath:(NSIndexPath *)indexPath
{
if ([cell isKindOfClass:[UYLTextCell class]])
{
UYLTextCell *textCell = (UYLTextCell *)cell;
textCell.numberLabel.text = [NSString stringWithFormat:@"Line %ld",(long)indexPath.row+1];
textCell.numberLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleCaption1];
textCell.lineLabel.text = [self.sourceData objectAtIndex:indexPath.row];
textCell.lineLabel.font = [UIFont preferredFontForTextStyle:UIFontTextStyleBody];
}
}

上面我们设置了两个Label的字体,万一用户想要修改他们的字体大小。

处理动态文本的变化

tableView应该在用户改变字体大小之后就被重新加载。我们将在ViewDidLoad中将ViewController设置为一个观察者(observer)来观察UIContentSizeCategoryDidChangeNotification消息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
- (void)viewDidLoad
{
[super viewDidLoad];
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(didChangePreferredContentSize:)
name:UIContentSizeCategoryDidChangeNotification object:nil];
}
- (void)dealloc
{
[[NSNotificationCenter defaultCenter] removeObserver:self
name:UIContentSizeCategoryDidChangeNotification
object:nil];
}
- (void)didChangePreferredContentSize:(NSNotification *)notification
{
[self.tableView reloadData];
}

计算Cell的高度

每一行的行高,我们需要使用tableView:heightForRowAtIndexPath方法。然而当这个方法被调用的时候,我们还没设置单元格的内容,所以很难设置他的高度。正如前面所提到的解决cell没有被加载但是应该调用他的布局,从而确定出他所需要的高度。所以我们增加了UYLTextCell来保存原型Cell的相关内容

1
2
3
4
@interface UYLTableViewController ()
...
@property (nonatomic, strong) UYLTextCell *prototypeCell;
@end

这个Cell原型将在getter方法中请求tableView使用入队一个新的Cell

1
2
3
4
5
6
7
8
- (UYLTextCell *)prototypeCell
{
if (!_prototypeCell)
{
_prototypeCell = [self.tableView dequeueReusableCellWithIdentifier:UYLCellIdentifier];
}
return _prototypeCell;
}

完整的delegate来计算每一行的高度方法如下所示:

1
2
3
4
5
6
7
8
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
[self configureCell:self.prototypeCell forRowAtIndexPath:indexPath];
[self.prototypeCell layoutIfNeeded];
CGSize size = [self.prototypeCell.contentView systemLayoutSizeFittingSize:UILayoutFittingCompressedSize];
return size.height+1;
}

这里有一些内容提要:

  • 使用configureCell方法来根据内容配置cell,从而得到对应的行。就像tableView:cellForRowAtIndexPath:方法一样在cell显示之前配置。
  • 调用layoutIfNeed来立即布局cell
  • 调用systemLayoutSizeFittingSize方法来获得内容的最小的大小,从而创建合适的约束。(UILayoutFittingCompressedSize)
  • 记得为了cell的分割线增加一个点大小。

估计cell高度

当我们在前面的实例工程中正确的设置tableViewCell的大小的时候,这仍旧有一个大的问题存在,那就是tableView的重载。为了在真机调试中找到这个问题,我们在后台改变了修改了文字内容的大小。在我的测试设备上,这彻底阻挡了用户的界面,浙江话费几秒钟来重新计算2000行的文字。这里有一个解决方法在iOS7中:

1
2
3
4
- (CGFloat)tableView:(UITableView *)tableView estimatedHeightForRowAtIndexPath:(NSIndexPath *)indexPath
{
return UITableViewAutomaticDimension;
}

这个方法将快速的返回一个你所需要估计的cell的高度。因为这个方法是快速估计,所以不需要太多时间。产生一个准确的估计也可以根据不同文本的大小来进行动态的选择。

如果你无法正确的估计文本大小,同时想要不让用户察觉到你的表格正在被刷星,那就返回UITableViewAutomaticDimension这个系统默认值。

问题与设备方向变化(2014年3月更新)

正如评论中所指出的(对于提出的人我很感激。),这里有一个问题会造成返回错误的高度,当用户设备放下发生改变,这个问题的解决方法可以参考smileyborgGithub工程。同时这个Github的工程也指出了我的问题,解决方法是先确保cell的宽度设置为原图标的宽度,再计算高度。

1
2
3
4
5
6
7
8
- (CGFloat)tableView:(UITableView *)tableView heightForRowAtIndexPath:(NSIndexPath *)indexPath
{
[self configureCell:self.prototypeCell forRowAtIndexPath:indexPath];
self.prototypeCell.bounds = CGRectMake(0.0f, 0.0f, CGRectGetWidth(self.tableView.bounds), CGRectGetHeight(self.prototypeCell.bounds));
...
...
}

第二我们需要调用layoutSubView在我们自定义的cell类中来设置多行Label中我们想要的最大宽度。官方文档中的preferredMaxLayoutWidth提供了这些细节。

这个属性将会影响label的大小当设置的余数被应用的时候。在显示的时候,如果内容超过它的范围,那么额外的内容将会变成一行或者多行,因此会增加label的高度。

1
2
3
4
5
6
7
// UYLTextCell.m
- (void)layoutSubviews
{
[super layoutSubviews];
[self.contentView layoutIfNeeded];
self.lineLabel.preferredMaxLayoutWidth = CGRectGetWidth(self.lineLabel.frame);
}

写在最后

这是一篇长文章,我希望对你有用,如果想要代码你可以在github上找到